package de.medieninf.sensimg;

import java.io.IOException;
import java.util.Vector;

import javax.microedition.io.Connector;
import javax.microedition.sensor.ChannelInfo;
import javax.microedition.sensor.Data;
import javax.microedition.sensor.DataListener;
import javax.microedition.sensor.MeasurementRange;
import javax.microedition.sensor.SensorConnection;
import javax.microedition.sensor.SensorInfo;
import javax.microedition.sensor.SensorManager;

/**
 * Singleton, that centrally receives and distributes sensor data.
 * @author pb
 */
public final class SensorService {
    /**
     * The singleton instance of SensorService.
     */
    private static SensorService sensorService = new SensorService();
    /**
     * The local cached reference to the settings instance.
     */
    private Settings settings = Settings.getSettings();
    /**
     * the last read x acceleration data.
     */
    private volatile double x;
    /**
     * the last read y acceleration data.
     */
    private volatile double y;
    /**
     * the last read z acceleration data.
     */
    private volatile double z;
    /**
     * the smallest possible acceleration value.
     */
    private volatile double smallest = Double.MAX_VALUE;
    /**
     * the largest possible acceleration value.
     */
    private volatile double largest = -Double.MAX_VALUE;
    /**
     * Observer that get called on acceleration data changes.
     */
    private Vector observers = new Vector();
    /**
     * Lock object protecting observers.
     */
    private Object lock = new Object();
    /**
     * SensorDataListener interfacing to platform access for
     * acceleration data.
     */
    private volatile SensorDataListener sensorDataListener;
    /**
     * Call back in case settings data has changed.
     */
    private Runnable updater = new Runnable() {
        public void run() {
            Logger.log("getSensor: " + settings.getSensor()
                       + "  sensorDataListener: " + sensorDataListener);
            if (settings.getSensor() && sensorDataListener == null) {
                startMonitoring();
            } else if ((!settings.getSensor()) && sensorDataListener != null) {
                stopMonitoring();
            }
        }
    };
    /**
     * Constructing the singleton instance.
     */
    private SensorService() {
        settings.registerUpdate(updater);
    }
    /**
     * Getter for the singleton instance.
     * @return the singleton instance
     */
    public static SensorService getSensorService() {
        return sensorService;
    }

    /**
     * Registers a listener that then receives acceleration data.
     * @param al AccelerationListener
     */
    public void register(final AccelerationListener al) {
        synchronized (lock) {
            if (!observers.contains(al)) {
                Logger.log("registering AccelerationListener " + al);
                observers.addElement(al);
            }
        }
    }

    /**
     * Unregisters AccelerationListener if registered, do nothing otherwise.
     * @param al AccelerationListener
     */
    public void unregister(final AccelerationListener al) {
        synchronized (lock) {
            if (observers.contains(al)) {
                Logger.log("unregistering AccelerationListener " + al);
                observers.removeElement(al);
            }
        }
    }

    /**
     * Unregisters all registered AccelerationListeners.
     */
    public void unregisterAll() {
        synchronized (lock) {
            observers.removeAllElements();
        }
    }

    /**
     * Internally update all registered observers on acceleration
     * data change.
     * @param px int x value
     * @param py int y value
     * @param pz int z value
     */
    private void update(final double px, final double py, final double pz) {
        AccelerationListener[] als;
        synchronized (lock) {
            als = new AccelerationListener[observers.size()];
            observers.copyInto(als);
        }
        for (int i = 0; i < als.length; i++) {
            als[i].update(px, py, pz);
        }
    }

    /**
     * Starts monitoring and distributing acceleration data
     * to registered AccelerationListeners.
     */
    public void startMonitoring() {
        stopMonitoring();
        sensorDataListener = new SensorDataListener();
        sensorDataListener.start();
        Logger.log("started monitoring acceleration");
    }

    /**
     * Stops monitoring (and thus distributing) acceleration data.
     */
    public void stopMonitoring() {
        if (sensorDataListener != null) {
            sensorDataListener.stopIt();
            sensorDataListener = null;
        }
        Logger.log("stopped monitoring acceleration");
    }

    /**
     * Checks whether we are monitoring acceleration data.
     * @return true iff we are monitoring acceleration data.
     */
    public boolean isMonitoring() {
        if (sensorDataListener != null) {
            return true;
        }
        return false;
    }

    /**
     * Gets the smallest possible acceleration value.
     * @return the smallest possible acceleration value
     */
    public double getSmallest() {
        return smallest;
    }

    /**
     * Gets the largest possible acceleration value.
     * @return the largest possible acceleration value
     */
    public double getLargest() {
        return largest;
    }

    /**
     * Private class that takes care of getting acceleration data.
     * Just replace this for getting acceleration data any other way.
     */
    private class SensorDataListener extends Thread implements DataListener {
        /**
         * Check whether it is stopped or running.
         */
        private volatile boolean isStopped;
        /**
         * buffer size for reading acceleration data.
         * Just one, update often (too fast in emulator, but ok on device)
         */
        static final int BUFFER_SIZE = 1;

        /**
         * Open a connection to the platform acceleration data provider.
         * @return open SensorConnection
         */
        private SensorConnection openSensor() {
            boolean sensorFound = false;
            SensorInfo[] infos = SensorManager.findSensors("acceleration",
                                                           null);
            if (infos.length == 0) {
                return null;
            }
            int[] datatypes = new int[infos.length];
            int i = 0;
            String sensorURL = "";
            Logger.log("Searching TYPE_DOUBLE sensor...");
            while (!sensorFound) {
                ChannelInfo channelInfo = infos[i].getChannelInfos()[0];
                datatypes[i] = channelInfo.getDataType();
                if (datatypes[i] == ChannelInfo.TYPE_DOUBLE) {
                    sensorURL = infos[i].getUrl();
                    Logger.log("Sensor: " + sensorURL + ": TYPE_DOUBLE found.");
                    MeasurementRange[] mranges;
                    mranges = channelInfo.getMeasurementRanges();
                    for (int j = 0; j < mranges.length; j++) {
                        double min = mranges[i].getSmallestValue();
                        if (min < smallest) {
                            smallest = min;
                        }
                        double max = mranges[i].getLargestValue();
                        if (max > largest) {
                            largest = max;
                        }
                    }
                    Logger.log("Sensorrange: " + smallest + " < " + largest);
                    sensorFound = true;
                } else  {
                    i++;
                }
            }
            Logger.log("Sensor: " + sensorURL);
            try {
                return (SensorConnection) Connector.open(sensorURL);
            } catch (IOException ioe) {
                ioe.printStackTrace();
                return null;
            }
        }

        /**
         * the actual listener loop listening to the acceleration data
         * provided by the platform.
         */
        private synchronized void listen() {
            SensorConnection sensorConnection = openSensor();
            if (sensorConnection == null) {
                return;
            }
            try {
                sensorConnection.setDataListener(this, BUFFER_SIZE);
                while (!isStopped) {
                    try {
                        wait();
                    } catch (InterruptedException ie) {
                        Logger.log("SensorService: int. acc. listener");
                    }
                }
                sensorConnection.removeDataListener();
            } catch (IllegalMonitorStateException imse) {
            	// FindBugs error - is dubious indeed; but from sample. 
            	// Necessary?
                imse.printStackTrace();
            } catch (IllegalArgumentException iae) {
                iae.printStackTrace();
            }
            try {
                sensorConnection.close();
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
            if (isStopped) {
                sensorConnection = null;
            }
        }

        /**
         * Storage for acceleration values on each event change.
         */
        private double[][] doubleValues = new double[3][BUFFER_SIZE];
        /**
         * acceleration data, directions.
         */
        private double[] directions = new double[3];
        /**
         * Get acceleration data, directions.
         * @param data the globally defined directions
         * @return the globally defined directions
         */
        private double[] getDirections(final Data[] data) {
            // Average as in example.
            // However, with buffer one shouldn't have impact.
            for (int i = 0; i < 3; i++) {
                doubleValues[i] = data[i].getDoubleValues();
                double temp = 0;
                for (int j = 0; j < BUFFER_SIZE; j++) {
                    temp = temp + doubleValues[i][j];
                }
                directions[i] = temp / BUFFER_SIZE;
            }
            return directions;
        }

        /**
         * Time, when the last packet has been actually used.
         */
        private volatile long lastPacket = 0;
        /**
         * How much packets maximal per second.
         */
        static final long PPS = 40;
        /**
         * How long to pause, in msecs, depending on PPS.
         */
        static final long MIN_PAUSE_MSECS = 1000 / PPS;
        /**
         * How many packets have been ignored.
         */
        private int ignored = 0;
        /**
         * The actual listener call back.
         * @param sensor open SensorConnection
         * @param data Data[] received
         * @param isDataLost whether or not
         */
        public void dataReceived(final SensorConnection sensor,
                                 final Data[] data,
                                 final boolean isDataLost) {
            if (System.currentTimeMillis() - lastPacket < MIN_PAUSE_MSECS) {
                ignored += 1;
                return; // ignore - basically for emulator
                // How can I reduce the rate this function is called?
                // This is more like a busy wait than a proper callback...
            }
            lastPacket = System.currentTimeMillis();
            double[] direcs = getDirections(data);
            x = direcs[0];
            y = direcs[1];
            z = direcs[2];
            update(x, y, z);
        }

        /**
         * Stop listening to platform provided acceleration data.
         */
        public synchronized void stopIt() {
            isStopped = true;
            notify();
        }

        /**
         * Start listening to platform provided acceleration data.
         */
        public void run() {
            listen();
        }
    }

}